Version 28.04.22
Este trabajo engloba la creación de un modelo de detección de noticias falsas. Desde el propio análisis de los datos hasta la elección y desarrollo de un modelo final. Además, se incluye un desarrollo de un topic modelling adicional.
El trabajo realizado en este report puede encontrarse en detalle en el repositorio de github https://github.com/carviagu/fake_news_classifier_model
En esta sección nos centramos en entender y analizar los datos a utilizar para elaborar el modelo de detección de noticias falsas. Para ello procederemos a identificar las principales características del conjunto de datos y determinar como podemos utilizarlo y procesarlo para crear el modelo final.
# Libraries
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split
import re
import string
from sklearn import preprocessing
import nltk
from nltk.tokenize import word_tokenize
from collections import Counter
#nltk.download('punkt')
#nltk.download('stopwords')
Procedemos a leer y visualizar los primeros cinco valores del dataset original a utilizar...
# Data reading
raw_data_df = pd.read_csv("../data/raw/fake_or_real_news.csv")
raw_data_df.head()
| Unnamed: 0 | title | text | label | |
|---|---|---|---|---|
| 0 | 8476 | You Can Smell Hillary’s Fear | Daniel Greenfield, a Shillman Journalism Fello... | FAKE |
| 1 | 10294 | Watch The Exact Moment Paul Ryan Committed Pol... | Google Pinterest Digg Linkedin Reddit Stumbleu... | FAKE |
| 2 | 3608 | Kerry to go to Paris in gesture of sympathy | U.S. Secretary of State John F. Kerry said Mon... | REAL |
| 3 | 10142 | Bernie supporters on Twitter erupt in anger ag... | — Kaydee King (@KaydeeKing) November 9, 2016 T... | FAKE |
| 4 | 875 | The Battle of New York: Why This Primary Matters | It's primary day in New York and front-runners... | REAL |
Observamos que los datos se componen de 4 campos, cada registro posee un valor identificativo (id), el título de la noticia, el contenido del mismo y la etiqueta, es decir, nuestra variable objetivo que nos muestra si la noticia es verdadera o falsa.
Procedemos a determinar las características sobre todo el conjunto de datos de la variable objetivo, centrándonos en su proproción en totat...
Nos diseñamos una función para obtener un resumen de la composición del dataset
def classes_overview(df = None, obj_val = ""):
'''
Devuelve un dataframe con el porcentaje y valor abosoluto de las clases de la variable objetivo.
:param df: Datase
:param obj_val: Variable objetivo
:return: Dataframe
'''
temp = df[obj_val].value_counts(normalize=True).mul(100).rename('percentaje').reset_index()
temp_conteo = df[obj_val].value_counts().reset_index()
return pd.merge(temp, temp_conteo, on=['index'], how='inner')
classes_overview(raw_data_df, 'label')
| index | percentaje | label | |
|---|---|---|---|
| 0 | REAL | 50.055249 | 3171 |
| 1 | FAKE | 49.944751 | 3164 |
Observamos como la proporción de los datos es bastante equilibrada, lo que simplifica bastante la tarea de división del dataset a no ser necesario tener en cuenta una desequelibrio a la hora de entrenar el modelo.
Previamente a continuar con el análisis vamos a realizar la division de los datos en muestra de train (entrenamiento) y test...
# We divide values of the target label and the rest of data
X = raw_data_df.drop('label', axis=1)
Y = raw_data_df['label']
# We make the split between train and test
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.15, random_state=1234, stratify=Y)
# We save the original data divided
train_data_df = X_train
train_data_df['label'] = Y_train
train_data_df.to_csv("../data/interim/train.csv")
test_data_df = X_test
test_data_df['label'] = Y_test
test_data_df.to_csv("../data/interim/test.csv")
Una vez realizada la separación procedemos a realizar el análisi de los datos. Nos centramos únicamente en la muestra de entrenamiento (train).
print('Total de muestras del dataset: ', len(train_data_df.index))
Total de muestras del dataset: 5384
Tenemos un total de 5384 muestras, vamos a verificar que el balanceo del dataset es adecuado otra vez después de la división de train y test...
# We check the balance of the dataset again to be sure the balance is correct
classes_overview(train_data_df, 'label')
| index | percentaje | label | |
|---|---|---|---|
| 0 | REAL | 50.055721 | 2695 |
| 1 | FAKE | 49.944279 | 2689 |
Observamos que el buen balanceo del dataset se mantiene, teniende un 50% para noticias reales y otro 50% de noticias falsas aproximadamente. Ahora nos centramos en distintos aspectos a evaluar.
Primero. Comprobamos que no hay valores nulos...
# We check the existance of anomalies in the data like NULL values
temp_df = train_data_df.isnull().sum().reset_index()
temp_df.columns = ['column_name', 'NAs']
temp_df
| column_name | NAs | |
|---|---|---|
| 0 | Unnamed: 0 | 0 |
| 1 | title | 0 |
| 2 | text | 0 |
| 3 | label | 0 |
Observamos que no existen valores nulos en los datos, por lo que no es necesario eliminar o tratar estos.
Segundo. Comprobamos si pueden existir noticias repetidas...
# We check any duplicates
sum(train_data_df.duplicated())
0
Observamos que no hay noticias repetidas en los datos y contamos con noticias diferentes para poder entrenar.
Realizamos un primer procesado de los datos con el objetivo de poder realizar más análisis y a la vez preparar los datos para el modelado.
Comenzaremos con la variable objetivo, la etiqueta de noticia falsa y verdadera. Haremos uso de un encoding de etiquetas: Label Encoding, esto sustituirá FAKE por 0 y REAL por 1
lb = preprocessing.LabelEncoder()
train_data_df['label'] = lb.fit_transform(train_data_df['label'])
print(lb.classes_)
# Repeat encoding for test sample
test_data_df['label'] = lb.transform(test_data_df['label'])
['FAKE' 'REAL']
train_data_df
| Unnamed: 0 | title | text | label | |
|---|---|---|---|---|
| 1342 | 8196 | Voter Dreading Being Sent Over To Visibly Stup... | Trump Raises Concern Over Members Of Urban Com... | 0 |
| 1251 | 30 | Abortion bill dropped amid concerns of female ... | This item has been updated.\n\nHouse Republica... | 1 |
| 804 | 7216 | Internet Is On Fire With Speculation That Pode... | We Are Change \nEmails revealed by Wikileaks f... | 0 |
| 47 | 587 | Senate race rankings: Dems attack as GOP lays ... | The move would make it easier for the Trump ad... | 1 |
| 3888 | 103 | Starbucks baristas stop writing 'Race Together... | In a marketing fiasco that could rank right up... | 1 |
| ... | ... | ... | ... | ... |
| 502 | 1118 | Romney's timely Trump trolling | **Want FOX News First in your inbox every day?... | 1 |
| 2824 | 4423 | Megyn Kelly interrogates Tom Cotton on Iran le... | On Monday, Cotton drafted a letter — signed by... | 1 |
| 1768 | 9347 | CONTROVERSIAL NEW ‘ANTI-FAMINE’ GMO POTATO STR... | Home › HEALTH | US NEWS › CONTROVERSIAL NEW ‘A... | 0 |
| 4116 | 6239 | Meter Reader Knocks On Man’s Door, Reveals Sin... | Share This \nWhen a man heard a knock on his d... | 0 |
| 4944 | 9843 | ‘On Contact’: Chris Hedges and Medea Benjamin ... | ‘On Contact’: Chris Hedges and Medea Benjamin ... | 0 |
5384 rows × 4 columns
Ahora procedemos a limpiar los títulos y textos de las noticias, observamos en un primer vistazo varios signos de puntuación, símbolos y otros signos utilizados para indicar espacios en programación (ej. \n), procedemos a homgeneizar los datos a minusculas, eliminando espacios, sustituyendo puntuaciones mediante el uso de traducciones y expresiones regulares.
translator = str.maketrans('', '', string.punctuation)
train_data_df['proc_title'] = train_data_df.title.map(lambda x : re.sub(r"[^a-zA-Z0-9]", " ",x.strip().lower().translate(translator)))
train_data_df['proc_text'] = train_data_df.text.map(lambda x : re.sub(r"[^a-zA-Z0-9]", " ",x.strip().lower().translate(translator)))
test_data_df['proc_title'] = test_data_df.title.map(lambda x : re.sub(r"[^a-zA-Z0-9]", " ",x.strip().lower().translate(translator)))
test_data_df['proc_text'] = test_data_df.text.map(lambda x : re.sub(r"[^a-zA-Z0-9]", " ",x.strip().lower().translate(translator)))
train_data_df['proc_title']
1342 voter dreading being sent over to visibly stup...
1251 abortion bill dropped amid concerns of female ...
804 internet is on fire with speculation that pode...
47 senate race rankings dems attack as gop lays s...
3888 starbucks baristas stop writing race together ...
...
502 romneys timely trump trolling
2824 megyn kelly interrogates tom cotton on iran le...
1768 controversial new antifamine gmo potato stra...
4116 meter reader knocks on man s door reveals sini...
4944 on contact chris hedges and medea benjamin o...
Name: proc_title, Length: 5384, dtype: object
Una vez limpiado el texto vamos realizar un análisis de las palabras contenidas para determinar relaciones y visualizar aspectos interesantes y comunes según la variable objetivo. Primero lo haremos con los títulos de las noticias...
corpus_fake = ' '.join(train_data_df[train_data_df['label'] == 0].proc_title)
all_words_fake = word_tokenize(corpus_fake)
corpus_true = ' '.join(train_data_df[train_data_df['label'] == 1].proc_title)
all_words_true = word_tokenize(corpus_true)
# Cleaning all stopwords
stopwords = nltk.corpus.stopwords.words('english')
all_words_fake = [word for word in all_words_fake if word not in stopwords]
all_words_true = [word for word in all_words_true if word not in stopwords]
Veamos que palabras son más comunes en cada caso...
fig, axes = plt.subplots(1, 2, figsize=(10, 10), sharey=False)
ax1 = pd.Series(Counter(all_words_fake)).sort_values(ascending=False).iloc[:30].plot(kind="bar", ax=axes[0]);
ax1.title.set_text("Fake News Top 30 words by title")
ax2 = pd.Series(Counter(all_words_true)).sort_values(ascending=False).iloc[:30].plot(kind="bar", ax=axes[1]);
ax2.title.set_text("True News Top 30 words by title")
Observamos como existen varias palabras comunes en ambos tipos de noticias, sin embargo tienen distinta frecuencia de aparición según el tipo de noticia. Por otro lado al ser noticias de misma tipología no resulta extraña esta similitud.
Veamos por otro lado las longitudes de los títulos...
titles_len = [len(i.split()) for i in train_data_df['proc_title']]
pd.Series(titles_len).hist(bins = 60);
plt.xlabel('Number of Words');
plt.ylabel('Number of Texts');
Ahora repetiremos este análisis con el contenido de las noticias...
corpus_fake = ' '.join(train_data_df[train_data_df['label'] == 0].proc_text)
all_words_fake = word_tokenize(corpus_fake)
corpus_true = ' '.join(train_data_df[train_data_df['label'] == 1].proc_text)
all_words_true = word_tokenize(corpus_true)
# Cleaning all stopwords
stopwords = nltk.corpus.stopwords.words('english')
all_words_fake = [word for word in all_words_fake if word not in stopwords]
all_words_true = [word for word in all_words_true if word not in stopwords]
fig, axes = plt.subplots(1, 2, figsize=(10, 10), sharey=False)
ax1 = pd.Series(Counter(all_words_fake)).sort_values(ascending=False).iloc[:30].plot(kind="bar", ax=axes[0]);
ax1.title.set_text("Fake News Top 30 words by content")
ax2 = pd.Series(Counter(all_words_true)).sort_values(ascending=False).iloc[:30].plot(kind="bar", ax=axes[1]);
ax2.title.set_text("True News Top 30 words by content")
Podemos observar como las palabras son bastante comunes en ambos casos, siendo las primeras ocurrencias casi iguales a las de los títulos.
En este notebook nos centramos en elaborar el modelo que utilizaremos para detectar las noticias verdaderas y falsas.
# Libraries
import pandas as pd
import string
import re
import nltk
from nltk.stem import WordNetLemmatizer
from sklearn import preprocessing
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
#nltk.download('wordnet')
# Train data
X_train = pd.read_csv("../data/interim/train.csv")
Y_train = X_train['label']
X_train = X_train['title']
# Test data
X_test = pd.read_csv("../data/interim/test.csv")
Y_test = X_test['label']
X_test = X_test['title']
Realizaremos la limpieza de los datos, seguiremos el mismo procedimiento que en el análisis realizado. Crearemos una función que realizará la limpieza de los datos para cada fila del dataset, además incorporaremos la lematización con el objetivo de reducir la variabilidad de las palabras, verbos especialmente que permitirán manejar un menor vocabulario al modelo.
# Translator to clean data
translator = str.maketrans('', '', string.punctuation)
# Word lemmatizer
wordnet_lemmatizer = WordNetLemmatizer()
# Stopwords
stopwords = nltk.corpus.stopwords.words('english')
def title_cleaner(tlt):
"""
Cleans the text passed. All text to lower, blanck spaces and punctuation.
Also eliminates stopwords and lematize verbs
:param tlt: text to clean
:type tlt: string
"""
# Lower letters and blank spaces out, we eliminate all type of punctuation
# Finally we split the data (tokenize)
tokens = re.sub(r"[^a-zA-Z0-9]", " ",tlt.strip().lower().translate(translator)).split()
# Lemmatize and eliminate any kind of stopword that doesn't gives us any information
final_words = [wordnet_lemmatizer.lemmatize(word, pos="v") for word in tokens if not word in stopwords]
# Joining the resulted words cleaned
return " ".join(final_words)
Aplicamos la función para limpiar los datos (tanto en train como en test)
X_train = X_train.map(lambda x : title_cleaner(x))
X_test = X_test.map(lambda x : title_cleaner(x))
Ahora codificamos la variable objetivo mediante LabelEncoding, es decir, 0 - FAKE y 1 - REAL
lb = preprocessing.LabelEncoder()
Y_train = lb.fit_transform(Y_train)
Y_test = lb.transform(Y_test)
print(lb.classes_)
['FAKE' 'REAL']
Ahora procedemos a crear las variables con los datos que serán utilizadas para el clasificador de noticias. Utilizaremos la técnica del Bag of Words y también la combinaremos con la técnica del TF-IDF. Crearemos muestras con solo el BoW y otra con el TF-IDF para poder utilizar cada una según el modelo y como consideremos oportuno.
# Model for creating variables
corpus = X_train
cv = CountVectorizer()
tft = TfidfTransformer()
# Creating the train result
cv_matrix = cv.fit_transform(corpus)
tfd_result = tft.fit_transform(cv_matrix)
# Preparing train data
# BoW only
features_cv= cv.get_feature_names_out()
X_train_bow = pd.DataFrame(cv_matrix.toarray(), columns=features_cv)
# Tfd added
features_tft = tft.get_feature_names_out()
X_train_tfd = pd.DataFrame(tfd_result.toarray(), columns=features_tft)
# Preparing test data
# BoW only
data_matrix = cv.transform(X_test)
X_test_bow = pd.DataFrame(data_matrix.toarray(), columns=features_cv)
# Tfd added
X_test_tfd = pd.DataFrame(tft.transform(data_matrix).toarray(), columns=features_tft)
Una vez procesados guardamos los datos
filenames = ["train_bow.csv", "train_tfd.csv", "test_bow.csv", "test_tfd.csv"]
samples = [X_train_bow, X_train_tfd, X_test_bow, X_test_tfd]
targets = [Y_train, Y_train, Y_test, Y_test]
for i in range(4):
sample = samples[i]
sample['label'] = targets[i]
sample.to_csv("../data/processed/" + filenames[i])
A continuación vamos a crear los distintos modelos para evaluar la detección de noticias falsas
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pickle
import string
import re
import nltk
from nltk.stem import WordNetLemmatizer
from sklearn import preprocessing
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.pipeline import Pipeline
# Metrics
from sklearn import metrics
from sklearn.model_selection import cross_val_score
from sklearn.metrics import ConfusionMatrixDisplay, classification_report, roc_curve, fbeta_score
import scikitplot as skplt
# Models
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import MultinomialNB
from sklearn import tree
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
Ahora procederemos a crear las funciones que agilizaran la ejecución y evaluación de los modelos. Las siguientes funciones nos permitiran graficar las matrices de confusión y curvas del modelo. No lo haremos para todos pero si para aquellos que consideremos relevantes.
# Ploting functions for the models
def confusion_matrix(titles_options = None, test = None, pred = None, labels = None):
'''
Prints confusion matrix making the predictions
:param titles_options:
:param test:
:param pred:
:param labels:
:return:
'''
for title, normalize in titles_options:
disp = metrics.ConfusionMatrixDisplay.from_predictions(
test,
pred,
display_labels=labels,
cmap=plt.cm.Blues,
normalize=normalize
)
disp.ax_.set_title(title)
print(title)
print(disp.confusion_matrix)
plt.show()
def model_curves_summary(test = None, pred = None, label = 'Model'):
'''
Prints a plot report with four different plots: ROC curve, PR curve,
Gain curve, Lift curve
:param test: Real values
:param pred: Predicted values
:param label: Model name
'''
fig, axes = plt.subplots(2, 2, figsize=(15, 10), sharey=False)
fig.suptitle('Curvas del modelo ' + label)
# ROC CURVE
fpr, tpr, thresholds = metrics.roc_curve(test, pred[:,1])
# plot the roc curve for the model
axes[0,0].plot([0,1], [0,1], linestyle='--', label='No Skill')
axes[0,0].plot(fpr, tpr, marker='.', label=label)
# axis labels
axes[0,0].set_title('Curva ROC')
axes[0,0].set(xlabel='False Positive Rate', ylabel='True Positive Rate')
axes[0,0].legend()
axes[0,0].grid()
# Precission-Recall CURVE
# calculate pr-curve
precision, recall, thresholds = metrics.precision_recall_curve(test, pred[:,1])
no_skill = len(test[test==1]) / len(test)
axes[0,1].plot([0,1], [no_skill,no_skill], linestyle='--', label='No Skill')
axes[0,1].plot(recall, precision, marker='.', label=label)
# axis labels
axes[0,1].set_title('Curva Precission-Recall')
axes[0,1].set(xlabel='Precission', ylabel='Recall')
axes[0,1].legend()
axes[0,1].grid()
# GAIN CURVE
skplt.metrics.plot_cumulative_gain(test, pred, ax = axes[1,0])
# LIFT CURVE
skplt.metrics.plot_lift_curve(test, pred, ax = axes[1,1])
Ahora crearemos una función de evaluación de los modelos para generar una tabla resumen de su funcionamiento. Esta función entrenará el modelo y lo evaluarará devolviendo para cada uno los valores el accuracy, la precisión, el recall. Además calcularemos también el F-score y el area de la curva ROC (AUC).
# Metrics list
models_report_df = pd.DataFrame(columns=['Model', 'Accuracy', 'Precission', 'Recall', 'F-score', 'AUC'])
# Model final list for later use
models_collection = {}
def model_train_test(model, traind, traind_labels, testd, testd_labels, data_info, i):
model_name = str(type(model).__name__) + data_info
# Training model
print("Training model... " + model_name)
model.fit(traind, traind_labels)
# Prediction with model
print("Predicting with model... " + model_name)
y_pred = model.predict(testd)
# Model information
models_report_df.loc[i] = [
model_name,
metrics.accuracy_score(testd_labels, y_pred),
metrics.precision_score(testd_labels, y_pred),
metrics.recall_score(testd_labels, y_pred),
metrics.f1_score(testd_labels, y_pred),
metrics.roc_auc_score(testd_labels, y_pred)
]
# Saving the model
models_collection[model_name] = model
print("Ended evaluation of model... " + model_name)
Ahora recuperaremos los datos para evaluar a los modelos. Realizaremos evaluaciones tanto con el preprocesado de Bag of Word como la versión que incluye el preprocesado Tfid de normalización de las frecuencias.
# Data preprocessed with BOW only
train_bow = pd.read_csv("../data/processed/train_bow.csv")
test_bow = pd.read_csv("../data/processed/test_bow.csv")
Y_train_bow = train_bow['label']
X_train_bow = train_bow.drop(['label'], axis=1)
Y_test_bow = test_bow['label']
X_test_bow = test_bow.drop(['label'], axis=1)
# Data preprocessed with BOW and TFD
train_tfd = pd.read_csv("../data/processed/train_tfd.csv")
test_tfd = pd.read_csv("../data/processed/test_tfd.csv")
Y_train_tfd = train_tfd['label']
X_train_tfd = train_tfd.drop(['label'], axis=1)
Y_test_tfd = test_tfd['label']
X_test_tfd = test_tfd.drop(['label'], axis=1)
Primero comenzaremos con los modelos para los datos con preprocesado BOW, en este caso utilizaremos un Árbol de decisión como modelo básico, un RandomForest, un Multinomial Naive Bayes y un clasificador GradientBosting como modelos más avanzados.
models = [tree.DecisionTreeClassifier(), RandomForestClassifier(), MultinomialNB(), GradientBoostingClassifier()]
for i, m in enumerate(models):
model_train_test(m, X_train_bow, Y_train_bow, X_test_bow, Y_test_bow, "_bow", i)
Training model... DecisionTreeClassifier_bow Predicting with model... DecisionTreeClassifier_bow Ended evaluation of model... DecisionTreeClassifier_bow Training model... RandomForestClassifier_bow Predicting with model... RandomForestClassifier_bow Ended evaluation of model... RandomForestClassifier_bow Training model... MultinomialNB_bow Predicting with model... MultinomialNB_bow Ended evaluation of model... MultinomialNB_bow Training model... GradientBoostingClassifier_bow Predicting with model... GradientBoostingClassifier_bow Ended evaluation of model... GradientBoostingClassifier_bow
Ahora generaremos modelos con la implementación de TFID, en este caso utilizaremos los mismos modelos e incorporaremos un Regresor Logístico que podremos utilizar al tener unos datos más normalizados y escalados.
models = [tree.DecisionTreeClassifier(), RandomForestClassifier(), MultinomialNB(), GradientBoostingClassifier(), LogisticRegression()]
for i, m in enumerate(models):
model_train_test(m, X_train_tfd, Y_train_tfd, X_test_tfd, Y_test_tfd, "_tfid", i+4)
Training model... DecisionTreeClassifier_tfid Predicting with model... DecisionTreeClassifier_tfid Ended evaluation of model... DecisionTreeClassifier_tfid Training model... RandomForestClassifier_tfid Predicting with model... RandomForestClassifier_tfid Ended evaluation of model... RandomForestClassifier_tfid Training model... MultinomialNB_tfid Predicting with model... MultinomialNB_tfid Ended evaluation of model... MultinomialNB_tfid Training model... GradientBoostingClassifier_tfid Predicting with model... GradientBoostingClassifier_tfid Ended evaluation of model... GradientBoostingClassifier_tfid Training model... LogisticRegression_tfid Predicting with model... LogisticRegression_tfid Ended evaluation of model... LogisticRegression_tfid
Veamos los resultados de todos los modelos generados...
models_report_df
| Model | Accuracy | Precission | Recall | F-score | AUC | |
|---|---|---|---|---|---|---|
| 0 | DecisionTreeClassifier_bow | 0.763407 | 0.757700 | 0.775210 | 0.766355 | 0.763395 |
| 1 | RandomForestClassifier_bow | 0.811777 | 0.835214 | 0.777311 | 0.805223 | 0.811813 |
| 2 | MultinomialNB_bow | 0.819138 | 0.816667 | 0.823529 | 0.820084 | 0.819133 |
| 3 | GradientBoostingClassifier_bow | 0.762355 | 0.881098 | 0.607143 | 0.718905 | 0.762519 |
| 4 | DecisionTreeClassifier_tfid | 0.740273 | 0.717268 | 0.794118 | 0.753739 | 0.740217 |
| 5 | RandomForestClassifier_tfid | 0.814932 | 0.836323 | 0.783613 | 0.809111 | 0.814965 |
| 6 | MultinomialNB_tfid | 0.807571 | 0.781190 | 0.855042 | 0.816449 | 0.807521 |
| 7 | GradientBoostingClassifier_tfid | 0.761304 | 0.842975 | 0.642857 | 0.729440 | 0.761429 |
| 8 | LogisticRegression_tfid | 0.809674 | 0.840647 | 0.764706 | 0.800880 | 0.809721 |
Observamos que los datos son bastante prometedores para los modelos con BOW, y como algunos mejoran al utilizar TFid. Escogeremos evaluar el RandomForest y el multinomial Naive Bayes como principales modelos a considerar al tener los mejores resultados con el proeprocesado del Bag of Words. Veamos sus matrices de confusión y respectivas curvas para compararlos con mayor detenimiento...
titles_options = [
("Matriz de confusión", None),
("Matriz de confusión normalizada", "true"),
]
confusion_matrix(titles_options, Y_test_tfd, models_collection['RandomForestClassifier_tfid'].predict(X_test_tfd),
['FAKE', 'REAL'])
Matriz de confusión [[402 73] [103 373]] Matriz de confusión normalizada [[0.84631579 0.15368421] [0.21638655 0.78361345]]
titles_options = [
("Matriz de confusión", None),
("Matriz de confusión normalizada", "true"),
]
confusion_matrix(titles_options, Y_test_bow, models_collection['MultinomialNB_bow'].predict(X_test_bow),
['FAKE', 'REAL'])
Matriz de confusión [[387 88] [ 84 392]] Matriz de confusión normalizada [[0.81473684 0.18526316] [0.17647059 0.82352941]]
model_curves_summary(Y_test_tfd, models_collection['RandomForestClassifier_tfid'].predict_proba(X_test_tfd), "RandomForestClassifier")
model_curves_summary(Y_test_bow, models_collection['MultinomialNB_bow'].predict_proba(X_test_bow), "MultinomialNB")
Nos quedamos con el modelo Multinomial Naive Bayes por su equilibrio en las predicciones realizadas. Procedemos a generar el pipeline del modelo con las principales características del preprocesado para proceder a optimizarlo y generar el modelo final.
def cleaning_data(tlt_df):
# Translator to clean data
translator = str.maketrans('', '', string.punctuation)
# Word lemmatizer
wordnet_lemmatizer = WordNetLemmatizer()
# Stopwords
stopwords = nltk.corpus.stopwords.words('english')
return tlt_df.map(lambda x : " ".join(
[wordnet_lemmatizer.lemmatize(word, pos="v") for word in
re.sub(r"[^a-zA-Z0-9]", " ", x.strip().lower().translate(translator)).split() if not word in stopwords])
)
estimator_model = make_pipeline(CountVectorizer(), MultinomialNB())
Leemos los datos oiriginales de train para entrenar el modelo y optimizarlo, además definimos los principales hiperparámetros a modificar. Mediante GridSearch se realizará una validación cruzada de las distintas combinaciones y así determinar el mejor resultado. Nos centraremos en evaluar el Recall del modelo y mejoras las predicciones.
train_full = pd.read_csv("../data/interim/train.csv")
Y_train = train_full['label'].str.replace("FAKE", "0").str.replace("REAL", "1").map(lambda x : int(x))
X_train = cleaning_data(train_full.drop(['label'], axis=1)['title'])
param_grid = {
'countvectorizer__binary': [True, False],
"multinomialnb__alpha": [0.01, 0.05, 0.1, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 1, 1.25, 1.5, 1.75, 2],
"multinomialnb__fit_prior": [True, False]
}
search = GridSearchCV(estimator_model, param_grid, scoring="recall")
result = search.fit(X_train, Y_train)
print('Best Score: %s' % result.best_score_)
print('Best Hyperparameters: %s' % result.best_params_)
Best Score: 0.847495361781076
Best Hyperparameters: {'countvectorizer__binary': True, 'multinomialnb__alpha': 1.75, 'multinomialnb__fit_prior': True}
El mejor modelo posee los hiperparámetros que devuelve el tuneado. Ahora crearemos el modelo final en el pipeline con los hiperparámetros que hemos definido.
final_model = Pipeline(
[('vector', CountVectorizer(binary=True)),
('model', MultinomialNB(alpha=0.75, fit_prior=False))]
)
final_model.fit(X_train, Y_train)
Pipeline(steps=[('vector', CountVectorizer(binary=True)),
('model', MultinomialNB(alpha=0.75, fit_prior=False))])
Vamos a determiar el score final de accuracy del modelo que hemos obtenido finalmente.
test_full = pd.read_csv("../data/interim/test.csv")
Y_test = test_full['label'].str.replace("FAKE", "0").str.replace("REAL", "1").map(lambda x : int(x))
X_test = cleaning_data(test_full.drop(['label'], axis=1)['title'])
Y_pred = final_model.predict(X_test)
metrics.accuracy_score(Y_test, Y_pred)
0.8128286014721346
titles_options = [
("Matriz de confusión", None),
("Matriz de confusión normalizada", "true"),
]
confusion_matrix(titles_options, Y_test, Y_pred,
['FAKE', 'REAL'])
Matriz de confusión [[369 106] [ 72 404]] Matriz de confusión normalizada [[0.77684211 0.22315789] [0.1512605 0.8487395 ]]
model_curves_summary(Y_test, final_model.predict_proba(X_test), "Final Model")
Guardamos el modelo mediante pickle para poder usarlo más adelante y productivizarlo.
file_name = "../models/fake_class_model.sav"
pickle.dump(final_model, open(file_name, "wb"))
A continuación vamos a crear modelos de Topic Modelling para la clasificación de noticias. Haremos uso de la librería gensim
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from gensim.parsing.preprocessing import preprocess_string
from gensim.corpora import Dictionary
from gensim.models import ldamodel
import pyLDAvis
import pyLDAvis.gensim_models
Cargamos los datos de test sobre las noticias falsas. Aplicamos un preprocesado propio de gensim mediante preprocess_string, este pasará los textos a minúsculas, eliminará elementos extraños (acentos, signos de puntuación, ...), quitará valores numérios y finalmente los stop words para reducir la dimensionalidad de los datos.
test_full = pd.read_csv("../data/interim/test.csv")[['title', 'label']]
test_full['label'] = test_full['label'].str.replace("FAKE", "0").str.replace("REAL", "1").map(lambda x : int(x))
test_full['title'] = test_full.title.apply(preprocess_string)
Ahora creamos el diccionario de datos y el Bag of Words que permitirá contabilizar las ocurrencias de las palabras. Estos serán los inputos del modelo de temas a crear.
dictionary = Dictionary(test_full['title'])
bow_corpus = [dictionary.doc2bow(text) for text in test_full['title']]
Finalmente creamos el modelo LDA (Latent Dirichlet Allocation). Este asiginará un topic a cada frase mediante una asignación de probabilidades a cada una de las palabras y frases analizadas.
ldamodel = ldamodel.LdaModel(bow_corpus, num_topics=15, id2word = dictionary, passes=20)
Visualizaremos los resultados del modelo mediante un gráfico interactivo de pyLDAvis.
pyLDAvis.enable_notebook()
vis = pyLDAvis.gensim_models.prepare(ldamodel, bow_corpus, dictionary)
vis
C:\Users\carviagu\anaconda3\envs\NSD\lib\site-packages\pyLDAvis\_prepare.py:246: FutureWarning: In a future version of pandas all arguments of DataFrame.drop except for the argument 'labels' will be keyword-only. default_term_info = default_term_info.sort_values(
Cada uno de los gráficos muestra unas burbujas que son los distintos temas detectados. El tamaño nos muestra la cantidad de elementos sobre ese tema y la proximidad a otras burbujas indica la similitud entre temas. Podemos ver en el gráfico de barras la presencia de las principales palabras en cada tema.
Por ejemplo el tema más popular (1) tiene términos como trump, hillari entre otros como los más repetidos.